/*
 * Copyright 2002-2006 the original author or authors. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language governing permissions and limitations under the
 * License.
 */
package pikes.xml;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import pikes.core.Printable;
import pikes.core.PrintableCharSequence;
import pikes.util.Assert;
import pikes.util.Util;

/**
 * XML markup representing a tag. It can have attributes and can have child contents. It can be printed to a character
 * stream and can participate in a XML tree processing by {@link XMLVisitor}
 * @author Peter Bona
 */
public class XMLTag extends PrintableXMLMarkup {

	private CharSequence name = null;

	private CharSequence prefix = null;

	private Map<CharSequence, Printable> attributes = new LinkedHashMap<CharSequence, Printable>();

	private TagContent content = null;

	/**
	 * Creates am XML tag with a name. Example:<br>
	 * <code>
	 * XMLTag tag = new XMLTag("name");<br>
	 * Writer writer = new OutputStreamWriter(System.out);<br>
	 * tag.print(writer);<br>
	 * writer.flush();<br>
	 * </code> will print <br>
	 * <code>&lt;name/&gt;</code> <br>
	 * to the standard output.
	 * @param name Name of the tag
	 */
	public XMLTag(final CharSequence name) {
		Assert.hasText(name);
		this.name = name;
		content = new TagContent(name);
	}

	public XMLTag(final CharSequence prefix, final CharSequence name) {
		this(name);
		Assert.hasText(name);
		this.prefix = prefix;
	}

	/**
	 * Visits this XML tag. The order of method calls is the following:
	 * <ol>
	 * <li>{@link XMLVisitor#openingFirstTag(CharSequence)} with the name of this tag.</li>
	 * <li>{@link XMLVisitor#attribute(CharSequence,CharSequence)} for each attribute of this tag.</li>
	 * <li>If this tag has child content, then all children are visited in the order they were added to the tag.</li>
	 * <li>{@link XMLVisitor#closeSecondTag(CharSequence)}, if this tag has child content</li>
	 * </ol>
	 * @throws XMLVisitingException
	 */
	public final void visit(XMLVisitor xmlVisitor) throws XMLVisitingException {
		if (prefix != null) {
			xmlVisitor.openingFirstTag(prefix, name);
		}
		else {
			xmlVisitor.openingFirstTag(name);
		}

		for (Map.Entry<CharSequence, Printable> entry : attributes.entrySet()) {
			xmlVisitor.attribute(entry.getKey(), entry.getValue());
		}
		content.visit(xmlVisitor);
	}

	/**
	 * Adds attribute to this tag. Example:<br>
	 * <code>
	 * XMLTag tag = new XMLTag("name");<br>
	 * tag.addAttribute("key","value");<br>
	 * Writer writer = new OutputStreamWriter(System.out);<br>
	 * tag.print(writer);<br>
	 * writer.flush();<br>
	 * </code> will print <br>
	 * <code>&lt;name key=&quot;value&quot;/&gt;</code> <br>
	 * to the standard output.
	 * @param attributeName name of the attribute
	 * @param value value of the attribute
	 */
	public final void addAttribute(CharSequence attributeName, CharSequence value) {
		attributes.put(attributeName, new PrintableCharSequence(value));
	}

	public final void addAttribute(CharSequence attributeName, Printable value) {
		attributes.put(attributeName, value);
	}

	/**
	 * Adds tag as a content to this tag. Example:<br>
	 * <code>
	 * XMLTag tag = new XMLTag("name");<br>
	 * tag.addTag(new XMLTag("subChild"));<br>
	 * Writer writer = new OutputStreamWriter(System.out);<br>
	 * tag.print(writer);<br>
	 * writer.flush();<br>
	 * </code> will print <br>
	 * <code>&lt;name&gt;&lt;subChild/&gt;&lt;/name&gt;</code> <br>
	 * to the standard output.
	 */
	public final void addTag(XMLTag tag) {
		Assert.notNull(tag);
		content.add(tag);
	}

	/**
	 * Adds a markup as a content to this tag.
	 */
	public final void addChild(XMLMarkup markup) {
		Assert.notNull(markup);
		content.add(markup);
	}

	/**
	 * Adds text as a content to this tag. Example:<br>
	 * <code>
	 * XMLTag tag = new XMLTag("name");<br>
	 * tag.addCharData("subText");<br>
	 * Writer writer = new OutputStreamWriter(System.out);<br>
	 * tag.print(writer);<br>
	 * writer.flush();<br>
	 * </code> will print <br>
	 * <code>&lt;name&gt;subText&lt;/name&gt;</code> <br>
	 * to the standard output.
	 */
	public final void addText(CharSequence text) {
		Assert.notNull(text);
		content.add(new PrintableText(new PrintableCharSequence(text)));
	}

	public final void addText(Printable printable) {
		Assert.notNull(printable);
		content.add(new PrintableText(printable));
	}

	/**
	 * Calculates the hash as a sum of the name, attribute map and the content's hash.
	 */
	@Override
	public final int hashCode() {
		return name.hashCode() + attributes.hashCode() + content.hashCode();
	}

	/**
	 * Returns true if the name, the attributes (order independent) and the list of contents (order dependent) match.
	 * Otherwise returns false.
	 */
	@Override
	public final boolean equals(Object obj) {
		if (!(obj instanceof XMLTag)) {
			return false;
		}

		XMLTag tag = (XMLTag) obj;

		if (!name.equals(tag.name)) {
			return false;
		}

		if (!(attributes.size() == tag.attributes.size())) {
			return false;
		}

		for (Map.Entry<CharSequence, Printable> entry : attributes.entrySet()) {
			if (!tag.attributes.containsKey(entry.getKey())) {
				return false;
			}

			Printable thisValue = entry.getValue();
			Printable tagValue = tag.attributes.get(entry.getKey());

			if (!Util.equals(thisValue, tagValue)) {
				return false;
			}
		}

		if (!content.equals(tag.content)) {
			return false;
		}

		return true;

	}

	/**
	 * Adds comment as content to this tag.
	 * @param comment
	 */

	public final void addComment(CharSequence comment) {
		if (comment != null) {
			content.add(new Comment(comment));
		}
	}

}

class PrintableText extends PrintableXMLMarkup {

	private Printable printable = null;

	public PrintableText(final Printable printable) {
		super();
		this.printable = printable;
	}

	public final void visit(XMLVisitor xmlVisitor) throws XMLVisitingException {
		xmlVisitor.text(printable);
	}

	/**
	 * Returns the character sequence
	 */
	@Override
	public String toString() {
		StringBuilder builder = new StringBuilder();
		try {
			printable.print(builder);
			return builder.toString();
		}
		catch (IOException e) {
			return e.getMessage();
		}
	}

	/**
	 * Return the character sequence's hash code
	 */
	@Override
	public final int hashCode() {
		return toString().hashCode();
	}

	/**
	 * Uses {@link Util#equals(CharSequence, CharSequence)}
	 */
	@Override
	public final boolean equals(Object obj) {

		if ((obj instanceof CharSequence)) {
			return Util.equals(toString(), (CharSequence) obj);
		}

		if ((obj instanceof PrintableText)) {
			return Util.equals(printable, ((PrintableText) obj).printable);
		}

		return false;
	}

}

class Comment extends PrintableXMLMarkup {
	private CharSequence comment = null;

	/**
	 * The only constructor.
	 * @param comment The comment character string
	 */
	public Comment(final CharSequence comment) {
		Assert.notNull(comment);
		this.comment = comment;
	}

	/**
	 * Calls {@link XMLVisitor#comment(CharSequence)}
	 */
	public final void visit(XMLVisitor xmlVisitor) throws XMLVisitingException {
		xmlVisitor.comment(comment);

	}
}

class TagContent extends PrintableXMLMarkup {

	private CharSequence parentTagName = null;

	private List<XMLMarkup> children = new LinkedList<XMLMarkup>();

	/**
	 * The only constructor taking the parent tag's name.
	 * @param parentTagName Name of the parent tag.
	 */
	public TagContent(final CharSequence parentTagName) {
		Assert.hasText(parentTagName);
		this.parentTagName = parentTagName;
	}

	/**
	 * Adds a XML markup to the collection
	 * @param markup
	 */
	public final void add(XMLMarkup markup) {
		children.add(markup);
	}

	/**
	 * Visits this list of XML content. The order of method calls is the following:
	 * <ol>
	 * <li>{@link XMLVisitor#closingFirstTag(CharSequence, boolean)} with the name of the parent tag and whether the
	 * list of content is empty.</li>
	 * <li>If the list of child content is not empty, {@link XMLMarkup#visit(XMLVisitor)} is called for each child.</li>
	 * <li>If the list of child content is not empty, {@link XMLVisitor#closeSecondTag(CharSequence)} is called with
	 * the name of the parent tag.</li>
	 * </ol>
	 * @throws XMLVisitingException
	 */
	public final void visit(XMLVisitor xmlVisitor) throws XMLVisitingException {
		xmlVisitor.closingFirstTag(parentTagName, !children.isEmpty());
		if (!children.isEmpty()) {
			for (XMLMarkup markUp : children) {
				markUp.visit(xmlVisitor);
			}
			xmlVisitor.closeSecondTag(parentTagName);
		}

	}

	/**
	 * Calculated the hash code from the list of children and the parent tag's name.
	 * @return the hash code
	 */
	@Override
	public final int hashCode() {
		return children.hashCode() + parentTagName.hashCode();
	}

	/**
	 * @return true if the parent tag's name, the size of the content list equals and each element in the list returns
	 * true. Otherwise, returns false.
	 */
	@Override
	public final boolean equals(Object obj) {
		if (!(obj instanceof TagContent)) {
			return false;
		}

		TagContent content = (TagContent) obj;

		if (!(children.size() == content.children.size())) {
			return false;
		}

		for (int i = 0; i < children.size(); i++) {
			Printable child = children.get(i);
			Printable tagChild = content.children.get(i);

			if (!child.equals(tagChild)) {
				return false;
			}
		}

		return true;
	}
}
